package com.acme.javaschool.intense.sems.sem2concurrency.pool;

import com.acme.javaschool.intense.sems.sem2concurrency.pool.data.ObjectSource;
import com.acme.javaschool.intense.sems.sem2concurrency.pool.data.PoolableObject;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class QueuePool<T> extends BasePool<T> {

    private ArrayBlockingQueue<PoolableObject<T>> availableItems;
    private ConcurrentHashMap<PoolableObject<T>, Object> takenItems;

    public QueuePool(ObjectSource<T> source, int minBound, int maxBound) {
        super(source, minBound, maxBound);

        availableItems = new ArrayBlockingQueue<>(minBound);
        fillAvailableItems(minBound);

        takenItems = new ConcurrentHashMap<>();
    }

    @Override
    public PoolableObject<T> tryGet(long timeoutMillis) throws InterruptedException {
        try {
            lock.lock();
            PoolableObject<T> item = null;

            resizeIfNeeded();

            if (timeoutMillis >= 0) {
                item = availableItems.poll(timeoutMillis, TimeUnit.MILLISECONDS);
            } else {
                item = availableItems.take();
            }

            if (item != null) {
                if (getAvailableItemsCount() == shrinkSizeThreshold + 1) {
                    lastThresholdSizeTimeMillis = System.currentTimeMillis();
                }
                item.open();

                takenItems.put(item, Boolean.TRUE);
            }
            return item;
        } finally {
            lock.unlock();
        }

    }

    @Override
    protected void shrink() {
        if (getTakenItemsCount() > minBound) {
            availableItems = new ArrayBlockingQueue<>(getTakenItemsCount());
        } else {
            ArrayBlockingQueue<PoolableObject<T>> newAvailableItems =
                    new ArrayBlockingQueue<>(minBound);
            availableItems.drainTo(newAvailableItems, minBound - getTakenItemsCount());
            availableItems = newAvailableItems;
        }
    }

    @Override
    protected void expand() {
        int sizeDelta = maxBound - getBound();
        // should preserve space for the already issued items.
        availableItems = new ArrayBlockingQueue<>(maxBound, true, availableItems);
        fillAvailableItems(sizeDelta);
    }

    @Override
    protected int getTakenItemsCount() {
        return getBound() - getAvailableItemsCount();
    }

    @Override
    protected void fillAvailableItems(int sizeDelta) {
        for (int i = 0; i < sizeDelta; i++) {
            availableItems.add(source.newInstance());
        }
    }

    @Override
    public void release(PoolableObject<T> item) throws InterruptedException {
        if (takenItems.containsKey(item)) {
            item.close();
            takenItems.remove(item);
            availableItems.put(item);
        } else {
            throw new RuntimeException("Item does not exist.");
        }
    }

    @Override
    public int getAvailableItemsCount() {
        return availableItems.size();
    }

    @Override
    public synchronized int getBound() {
        return availableItems.remainingCapacity() + availableItems.size();
    }

    @Override
    public int getMaxBound() {
        return maxBound;
    }
}
